<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Neo</title>
  <style>
    html,
    body {
      height: 100%;
      margin: 0;
      overflow: hidden;
    }

    body {
      display: flex;
      align-items: center;
      justify-content: center;
      background: #000;
    }

    main {
      display: flex;
    }

    p {
      line-height: 1;
    }

    span {
      display: block;
      width: 2vmax;
      height: 2vmax;
      font-size: 2vmax;
      color: #9bff9b11;
      text-align: center;
      font-family: "Microsoft Yahei", Helvetica, "Helvetica Neue";
    }
  </style>
</head>

<body>
  <main></main>
  <script>
    function r(from, to) {
      return ~~(Math.random() * (to - from + 1) + from);
    }

    function pick() {
      return arguments[r(0, arguments.length - 1)];
    }

    function getChar() {
      return String.fromCharCode(pick(
        // 易经六十四卦符号
        r(0x4DC0, 0x4DFF),
        // 康熙字典部首
        r(0x2F00, 0x2FDF),
        // 几何图形
        r(0x25A0, 0x25FF),
        // 特殊
        r(0xFFF0, 0xFFFF)
      ));
    }

    function loop(fn, delay) {
      let stamp = Date.now();

      function _loop() {
        if (Date.now() - stamp >= delay) {
          fn();
          stamp = Date.now();
        }
        requestAnimationFrame(_loop);
      }
      requestAnimationFrame(_loop);
    }
    class Char {
      constructor() {
        this.element = document.createElement('span');
        this.mutate();
      }
      mutate() {
        this.element.textContent = getChar();
      }
    }
    class Trail {
      constructor(list = [], options) {
        this.list = list;
        this.options = Object.assign({
          size: 10,
          offset: 0
        }, options);
        this.body = [];
        this.move();
      }
      traverse(fn) {
        this.body.forEach((n, i) => {
          let last = (i == this.body.length - 1);
          if (n) fn(n, i, last);
        });
      }
      move() {
        this.body = [];
        let {
          offset,
          size
        } = this.options;
        for (let i = 0; i < size; ++i) {
          let item = this.list[offset + i - size + 1];
          this.body.push(item);
        }
        this.options.offset =
          (offset + 1) % (this.list.length + size - 1);
      }
    }
    class Rain {
      constructor({
        target,
        row
      }) {
        this.element = document.createElement('p');
        this.build(row);
        if (target) {
          target.appendChild(this.element);
        }
        this.drop();
      }
      build(row = 20) {
        let root = document.createDocumentFragment();
        let chars = [];
        for (let i = 0; i < row; ++i) {
          let c = new Char();
          root.appendChild(c.element);
          chars.push(c);
          if (Math.random() < .5) {
            loop(() => c.mutate(), r(1e3, 5e3));
          }
        }
        this.trail = new Trail(chars, {
          size: r(10, 30),
          offset: r(0, 100)
        });
        this.element.appendChild(root);
      }
      drop() {
        let trail = this.trail;
        let len = trail.body.length;
        let delay = r(10, 100);
        loop(() => {
          trail.move();
          trail.traverse((c, i, last) => {
            c.element.style = `
          color: hsl(136, 100%, ${85 / len * (i + 1)}%)
        `;
            if (last) {
              c.mutate();
              c.element.style = `
            color: hsl(136, 100%, 85%);
            text-shadow:
              0 0 .5em #fff,
              0 0 .5em currentColor;
          `;
            }
          });
        }, delay);
      }
    }
    
    const main = document.querySelector('main');
    for (let i = 0; i < 50; ++i) {
      new Rain({
        target: main,
        row: 50
      });
    }
  </script>
</body>

</html>